# -*- coding: cp1252 -*-
#"Uso:
#	python perceptron [parametros] entradas control salidas

import numpy
import random
import sys
import getopt
import math

def hardlim(n):
	if n<0: return 0
	else: return 1

def perceptron(inputs,outputs,cotaInfErr,max_iter,learning_rate):

	# supongo que inputs tiene dimensiones: nPatrones x nEntradas
	# => la traspuesta tiene dimensiones: nEntradas x nPatrones
	inp=(numpy.matrix(inputs)).T
	print inp, "\n"

	# supongo que la salida tiene dimensiones: nPatrones x nSalidas
	# => la traspuesta tiene dimensiones: nSalidas x nPatrones
	outp=(numpy.matrix(outputs)).T
	print outp, "\n"

	# numero de entradas (25)
	nEntradas=inp.shape[0]
	print "# entradas:",nEntradas

	# numero de patrones (26)
	nPatrones=inp.shape[1]
	print "# patrones:",nPatrones

	# numero de salidas/neuronas (5)
	nSalidas = outp.shape[0]
	print "# salidas:", nSalidas

	# para corroborar que matchee con nPatrones (26)
	print "# patrones:", outp.shape[1]

	#genera aleatoriamente la matriz de pesos W
	W=numpy.mat(numpy.random.rand(nSalidas,nEntradas))
	b=numpy.mat(numpy.ones((nSalidas,1)))

	E = cotaInfErr + 1
	i=0

	min_E = float('inf')
	min_W = W
	min_i = 0

	while (E>cotaInfErr and i<max_iter):

#		print i

		trainIndex=range(nPatrones)
		random.shuffle(trainIndex) #reordeno aleatoriamente los m<=p para que no los tome siempre igual

		# matriz de resultados antes de la func. para cada patron(columna) guardo: W*p+b
		h = numpy.mat(numpy.zeros((nSalidas,nPatrones)))

		# matriz de resultados despues de la func. para cada patron(columna) guardo: f(W*p+b)
		O = numpy.mat(numpy.zeros((nSalidas,nPatrones)))

		# matriz de diferencias con el resultado. Para cada patron(columna) guarda el delta de cada salida (fila)
		delta = numpy.mat(numpy.zeros((nSalidas,nPatrones)))

		E = 0

		# por cada patron
		for m in trainIndex:

			# Asigno a la columna m de h el valor que le corresponde (las salidas para ese patron)
			h[:,m] = W*inp[:,m] + b[:]

			# aplico la funcion a cada salida para cada patron (a cada elemento de la matriz h)
			for s in range(nSalidas):
				O[s,m] = hardlim(h[s,m])

#			dev=0 #hay que reemplazar con derivada de g

			# falta multiplicar por matriz que tenga en las diagonales las derivadas de g en h-i (en este caso es multiplicar por id)
			delta[:,m] = outp[:,m] - O[:,m]

			# esto da la normal del error al cuadrado
			E = E + 0.5*float(delta[:,m].T*delta[:,m])

		# si encuentro un mejor W (con menor error) lo actualizo
		if E<min_E:
			min_E = E
			min_W = W
			min_i = i

		# actualizo los pesos de la matriz

		for m in trainIndex:
			for s in range(nSalidas):
				for e in range(nEntradas):
					W[s,e] = W[s,e] + learning_rate * delta[s,m] * inp[e,m]
				b[s] = b[s] + learning_rate * delta[s,m] * -1

		i=i+1
	# end while

	# imprimo los resultados
	print "iteraciones:", i
	print "iteracion del ultimo error:", min_i
	print "minimo error:", min_E
#	print "minimo W:\n", min_W

	return min_W

# Hay que ver:
# 	* Correr con varios learning rates distintos 0<lr<=1 para ver como cambia el comportamiento y elegir uno adecuado  (el mejor?).
#	  tal vez habria que hacerlo con la misma matriz de pesos inicial. poner el mismo seed?
# 	* Analizar los resultados para conseguir una cota de iteraciónes razonable
# 	* Cual es una buena cota minima para el error?
# 	* numpy no esta levantando bien las matrices en el formato de los archivos de entrada con ruido. corregir
#	* Habría que definir conjuntos de validación y testeo. entrenar sin ruido y testear con? entrenar con ruido y testear sin?
#	* Se puede estudiar la diferencia del comportamiento al actualizar los pesos sincronica o asincronicamente

f_in = open("Letras/letras.in",'r')
f_out = open("Letras/letras.out",'r')

W = perceptron(f_in.read(),f_out.read(),1,500,0.75)

f_in.close()
f_out.close()
